package suncertify.service;

import suncertify.util.Notification;
import suncertify.util.Messages;
import suncertify.db.RecordNotFoundException;
import suncertify.db.DuplicateKeyException;

import java.util.*;

/**
 * This abstract class provides default implementations for
 * most of the common methods for data managing. It takes care
 * about local data snapshots and translating indexes in
 * recors ids
 */

public abstract class Service extends Observable {
    protected static final Messages msg = Messages.getInstance();
    private static final Long DEF_LOCK = -1L;

    /**
     * option name of server remote host
     */
    public static final String HOST = "service.remote.host";
    /**
     * option name of local database file path
     */
    public static final String DATA = "service.data";

    private int current;

    private Map<Integer, String[]> data = new HashMap<Integer, String[]>();
    private List<Integer> ids = new ArrayList<Integer>();

    private Map<Integer, Long> locks = new HashMap<Integer, Long>();

    /**
     * reads a cell data
     *
     * @param row row for read
     * @param col column for read
     * @return cell data
     */
    public String get(int row, int col) {
        String[] data = this.data.get(getId(row));
        return data == null ? "" : data[col];
    }

    /**
     * reads a current row
     *
     * @return row data
     */
    public String[] read() {
        return data.get(getId(current));
    }

    protected abstract String[] read(int id) throws RecordNotFoundException;

    /**
     * updates a currently selected row
     *
     * @param data updated data
     */
    public void update(String[] data) {
        Integer id = getId(current);
        Long lock = getLock(id);
        try {
            notify(ServiceEvent.UPDATING);
            update(id, lock, data);
            this.data.put(id, data);
            notify(ServiceEvent.UPDATED);
        } catch (RecordNotFoundException e) {
            notify(ServiceEvent.ROW_REMOVED, msg.get("db.error.row.deleted"), current);
        } catch (Exception e) {
            notify(ServiceEvent.ERROR, e.getMessage());
        }
    }

    protected abstract void update(Integer id, Long lock, String[] data) throws RecordNotFoundException;

    /**
     * deletes a currently selected row
     */
    public void delete() {
        Integer id = getId(current);
        Long lock = getLock(id);
        try {
            notify(ServiceEvent.DELETING);
            delete(id, lock);
            data.remove(id);
            ids.remove(id);
            locks.remove(id);
            notify(ServiceEvent.DELETED);
        } catch (RecordNotFoundException e) {
            notify(ServiceEvent.ROW_REMOVED, msg.get("db.error.row.deleted"), current);
        } catch (Exception e) {
            notify(ServiceEvent.ERROR, e.getMessage());
        }
    }

    protected abstract void delete(Integer id, Long lock) throws RecordNotFoundException;

    /**
     * searchs a rows matched criteria
     *
     * @param criteria search criteria
     */
    public void search(String[] criteria) {
        setResultData(find(criteria));
    }

    protected abstract int[] find(String[] criteria);


    /**
     * adds a new row to DB and to local data set
     *
     * @param data new row data
     */
    public void add(String[] data) {
        notify(ServiceEvent.CREATING);
        int id;
        try {
            id = create(data);
            if (id != -1) {
                this.ids.add(id);
                this.data.put(id, data);
                notify(ServiceEvent.CREATED);
            }
        } catch (Exception e) {
            notify(ServiceEvent.ERROR, e.getMessage());
        }
    }

    protected abstract int create(String[] data) throws DuplicateKeyException;

    /**
     * locks a current row
     */
    public void lock() {
        Integer id = getId(current);
        try {
            notify(ServiceEvent.LOCKING);
            Long lock = lock(id);
            if (lock > 0) {
                locks.put(id, lock);
                notify(ServiceEvent.LOCKED);
            }
        } catch (RecordNotFoundException e) {
            ids.remove(id);
            data.remove(id);
            notify(ServiceEvent.ROW_REMOVED, msg.get("db.error.row.deleted"), current);
        }
    }

    protected abstract Long lock(Integer id) throws RecordNotFoundException;

    /**
     * unlocks a current row
     */
    public void unlock() {
        Integer id = getId(current);
        if (id != null && locks.containsKey(id)) {
            try {
                notify(ServiceEvent.UNLOCKING);
                unlock(id, getLock(id));
                locks.remove(id);
                notify(ServiceEvent.UNLOCKED);
            } catch (RecordNotFoundException e) {
                notify(ServiceEvent.ROW_REMOVED, msg.get("db.error.row.deleted"), current);
            } catch (Exception e) {
                notify(ServiceEvent.ERROR, e.getMessage());
            }
        }
    }

    /**
     * tells can service work or not
     *
     * @return true if active
     */
    public abstract boolean isActive();

    /**
     * try to init the service
     */
    public abstract void init();


    /**
     * shutdown the service
     */
    public abstract void shutdown();

    /**
     * select a row
     *
     * @param current index in table
     */
    public void select(int current) {
        this.current = current;
    }

    /**
     * returns the index of selected row
     *
     * @return index of row
     */
    public int getCurrent() {
        return current;
    }

    /**
     * returns a row count
     *
     * @return row count
     */
    public int getRowCount() {
        return ids == null ? 0 : ids.size();
    }

    /**
     * returns a column count
     *
     * @return column count
     */
    public abstract int getColumnCount();

    protected void notify(ServiceEvent event, String message, Object data) {
        setChanged();
        notifyObservers(new Notification(event, message, data));
    }

    protected void notify(ServiceEvent event, Object data) {
        setChanged();
        notifyObservers(new Notification(event, data));
    }

    protected void notify(ServiceEvent event, String message) {
        setChanged();
        notifyObservers(new Notification(event, message));
    }

    protected void notify(ServiceEvent event) {
        setChanged();
        notifyObservers(new Notification(event));
    }

    private Integer getId(int recNo) {
        return recNo >= 0 && recNo < ids.size() ? ids.get(recNo) : -1;
    }

    private Long getLock(Integer id) {
        Long lock = locks.get(id);
        if (lock == null) {
            lock = DEF_LOCK;
        }
        return lock;
    }

    protected abstract void unlock(int id, Long lock) throws RecordNotFoundException;

    private void setResultData(int[] ids) {
        this.ids.clear();
        this.data.clear();
        notify(ServiceEvent.READING);
        for (int i : ids) {
            try {
                String[] rowData = read(i);
                if (rowData != null) {
                    this.ids.add(i);
                    this.data.put(i, rowData);
                }
            } catch (Exception e) {
                //do nothing
            }
        }
        notify(ServiceEvent.READED);
    }

    /**
     * returns column name
     *
     * @param col index of column
     * @return column name
     */
    public abstract String getColumnName(int col);

    /**
     * try to cancel current lock
     */
    public void cancelLocking() {
        cancelLocking(getId(current));
    }


    protected abstract void cancelLocking(Integer id);

    /**
     * refresh the current record value from the database
     */
    public void refresh() {
        Integer id = getId(current);
        if (id != null) {
            try {
                data.put(id, read(id));
            } catch (RecordNotFoundException e) {
                //do nothing
            }
        }
    }
}
